博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java 权限框架 Shiro 实战二:与spring集成、filter机制
阅读量:5879 次
发布时间:2019-06-19

本文共 26724 字,大约阅读时间需要 89 分钟。

Shiro和Spring的集成,涉及到很多相关的配置,涉及到shiro的filer机制以及它拥有的各种默认filter,涉及到shiro的权限判断标签,权限注解,涉及到session管理等等方面。

1. 配置

首先需要在web.xml中专门负责接入shiro的filter:

shiroFilter
org.springframework.web.filter.DelegatingFilterProxy
true
targetFilterLifecycle
true
shiroFilter
/*

并且需要放在所有filter中靠前的位置,比如需要放在siteMesh的过滤器之前。

DelegatingFilterProxy 表示这是一个代理filter,它会将实际的工作,交给spring配置文件中 id="shiroFilter" 的bean来处理:

public class DelegatingFilterProxy extends GenericFilterBean {    private String contextAttribute;    private WebApplicationContext webApplicationContext;    private String targetBeanName;    private boolean targetFilterLifecycle = false;    private volatile Filter delegate;    private final Object delegateMonitor = new Object();    @Override    protected void initFilterBean() throws ServletException {        synchronized (this.delegateMonitor) {            if (this.delegate == null) {                // If no target bean name specified, use filter name.                if (this.targetBeanName == null) {                    this.targetBeanName = getFilterName();                }                // Fetch Spring root application context and initialize the delegate early,                // if possible. If the root application context will be started after this                // filter proxy, we'll have to resort to lazy initialization.                WebApplicationContext wac = findWebApplicationContext();                if (wac != null) {                    this.delegate = initDelegate(wac);                }            }        }    }
public abstract class GenericFilterBean implements        Filter, BeanNameAware, EnvironmentAware, ServletContextAware, InitializingBean, DisposableBean {    @Override    public final void init(FilterConfig filterConfig) throws ServletException {        Assert.notNull(filterConfig, "FilterConfig must not be null");        if (logger.isDebugEnabled()) {            logger.debug("Initializing filter '" + filterConfig.getFilterName() + "'");        }        this.filterConfig = filterConfig;        // Set bean properties from init parameters.        try {            PropertyValues pvs = new FilterConfigPropertyValues(filterConfig, this.requiredProperties);            BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);            ResourceLoader resourceLoader = new ServletContextResourceLoader(filterConfig.getServletContext());            bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.environment));            initBeanWrapper(bw);            bw.setPropertyValues(pvs, true);        }        catch (BeansException ex) {            String msg = "Failed to set bean properties on filter '" +                filterConfig.getFilterName() + "': " + ex.getMessage();            logger.error(msg, ex);            throw new NestedServletException(msg, ex);        }        // Let subclasses do whatever initialization they like.        initFilterBean();        if (logger.isDebugEnabled()) {            logger.debug("Filter '" + filterConfig.getFilterName() + "' configured successfully");        }    }

// Let subclasses do whatever initialization they like.

initFilterBean();

Filter 接口的 init 方法调用 initFilterBean(), 而该方法在子类中进行实现,它先获得 this.targetBeanName = getFilterName(); bean的名称,也就是id,然后对其进行初始化:this.delegate = initDelegate(wac); 其实就是从bean工厂中根据bean的名称找到bean.

protected Filter initDelegate(WebApplicationContext wac) throws ServletException {        Filter delegate = wac.getBean(getTargetBeanName(), Filter.class);        if (isTargetFilterLifecycle()) {            delegate.init(getFilterConfig());        }        return delegate;    }

而 shiroFilter在spring中的配置如下:

/reg/** = anon
/login = authc /logout = logout /authenticated = authc /loginController = anon /js/** = anon /css/** = anon /img/** = anon /html/** = anon /font-awesome/** = anon
/** = user

上面的shiroFilter的配置又引出了 securityManager 和 shiro 的filter机制和他自带的一些filter.

2. securityManager 级相关配置

在上一篇文章 中我们知道securityManager是shiro的顶层对象,它管理和调用其它所有子系统,负责系统的安全。我们知道shiro有两个类型的securityManager一个是JavaSE环境,默认是DefaultSecurityManager一个是web环境,默认是DefaultWebSecurityManager。所以我们web环境肯定应该使用后者。我们从顶层对象一层一层向下配置。先看securityManager如何配置:

上面的配置相当于调用SecurityUtils.setSecurityManager(securityManager) ,来注入了下面配置的 securityManager(DefaultWebSecurityManager) :

它默认使用的session管理器是 ServletContainerSessionManager,所以上面没有配置,所以就使用默认值。配置了就会覆盖下面的默认值:

public DefaultWebSecurityManager() {        super();        ((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(new DefaultWebSessionStorageEvaluator());        this.sessionMode = HTTP_SESSION_MODE;        setSubjectFactory(new DefaultWebSubjectFactory());        setRememberMeManager(new CookieRememberMeManager());        setSessionManager(new ServletContainerSessionManager());    }

显然 securityManager 最重要的工作就是用户登录认证和获得用户的权限等相关信息,所以 realm 是其最重要的依赖:

要理解上面userRealm的配置,就的先理解 UserRealm 的继承体系:

UserRealm 继承  AuthorizingRealm 显然是为了获取权限信息,对用户进行访问控制;继承AuthenticatingRealm显然是为了获得用户的认证信息,对用户进行认证。而 credentialsMatcher 就是 AuthenticatingRealm 使用来进行密码验证的依赖的组件:

public abstract class AuthenticatingRealm extends CachingRealm implements Initializable {
/** * Credentials matcher used to determine if the provided credentials match the credentials stored in the data store. */ private CredentialsMatcher credentialsMatcher;

再看其credentialsMatcher bean的配置:

配置就是 hash加密的相关参数:hash算法,hash迭代次数等。到这里 shiro 登录验证的配置就完了。至于获取用户信息和用户的权限的信息,都在userRealm中实现了:

public class UserRealm extends AuthorizingRealm {    @Autowired    private UserService userService;    @Override    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {        String userName = (String)principals.getPrimaryPrincipal();        User user = userService.getUserByUserName (userName );        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();        authorizationInfo.setRoles(userService.findRolesByUserId(user.getId()));        authorizationInfo.setStringPermissions(userService.findPermissionsByUserId(user.getId()));        return authorizationInfo;    }        @Override    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {        String userName = (String)token.getPrincipal();        User user = userService.getUserByUserName(userName);        if(user == null) {            throw new UnknownAccountException();//没找到账户        }        if(user.getLocked() == 0) {            throw new LockedAccountException(); //帐号锁定        }        if(user.getLocked() == 2){            throw new AuthenticationException("account was inactive");        }        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(                user.getUserName(),                user.getPassword(), // 密码                ByteSource.Util.bytes(user.getCredentialsSalt()),    // salt                getName()  // realm name        );                return authenticationInfo;    }

securityManager会在需要的时候回调上面 的 doGetAuthorizationInfodoGetAuthenticationInfo 方法,从realm中获得登录认证信息和用户权限信息。至于 rememberMeManager 主要是实现使用cookie表示我已经登录过了,下次不需要重新登录,这一个功能,也就是“记住我”登录过这一功能:

还有cacheManager的配置:

使用的是 EhCache.

3. Shiro 的filter机制和自带的filter

Shiro的filter是基于Servlet的Filter接口实现的。我们通过Shiro提供的form登录filter:FormAuthenticationFilter 和 ShiroFilter 看看其实现:

 

继承中的每一层都实现了一些功能:

1> NameableFilter:实现给filter取名的功能(Allows a filter to be named via JavaBeans-compatible)

/** * Allows a filter to be named via JavaBeans-compatible*/public abstract class NameableFilter extends AbstractFilter implements Nameable {    /**     * The name of this filter, unique within an application.     */    private String name;

2> OncePerRequestFilter : 保证对于同一个request,fiter只执行一次(Filter base class that guarantees to be just executed once per request)

/** * Filter base class that guarantees to be just executed once per request, * on any servlet container. It provides a {
@link #doFilterInternal} * method with HttpServletRequest and HttpServletResponse arguments.*/public abstract class OncePerRequestFilter extends NameableFilter {

3> AdviceFilter: SpringMVC风格的过滤器(就是preHandle, postHandle,afterCompletion 三接口的过滤器)

/**  * A Servlet Filter that enables AOP-style "around" advice for a ServletRequest via  * preHandle(javax.servlet.ServletRequest, javax.servlet.ServletResponse),  * postHandle(javax.servlet.ServletRequest, javax.servlet.ServletResponse),  * and afterCompletion(javax.servlet.ServletRequest, javax.servlet.ServletResponse, Exception)hooks.  */ public abstract class AdviceFilter extends OncePerRequestFilter {
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { return true; } @SuppressWarnings({
"UnusedDeclaration"}) protected void postHandle(ServletRequest request, ServletResponse response) throws Exception { } @SuppressWarnings({
"UnusedDeclaration"}) public void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception { }

4> PathMatchingFilter:该过滤器仅仅处理指定的路径(比如上面的配置:/js/** = anon,表示对 /js/ 目录和其子目录的请求,交给anon过滤器处理)

/** * 

Base class for Filters that will process only specified paths and allow all others to pass through.

*/public abstract class PathMatchingFilter extends AdviceFilter implements PathConfigProcessor {

5> AccessControlFilter: 实现提供对资源的访问控制,没有权限时,重定向到登录页面,登录之后跳转到原来的那个页面

/** * Superclass for any filter that controls access to a resource and may redirect the user to the login page * if they are not authenticated.  This superclass provides the method * saveRequestAndRedirectToLogin(javax.servlet.ServletRequest, javax.servlet.ServletResponse) * which is used by many subclasses as the behavior when a user is unauthenticated.*/public abstract class AccessControlFilter extends PathMatchingFilter {

6> AuthenticationFilter: 实现对访问用户的认证要求,也就是必须登录了才能访问

/** * Base class for all Filters that require the current user to be authenticated. This class encapsulates the * logic of checking whether a user is already authenticated in the system while subclasses are required to perform * specific logic for unauthenticated requests.*/public abstract class AuthenticationFilter extends AccessControlFilter {

7> AuthenticatingFilter: 实现判断用户是否有权限访问某资源。

/** * An AuthenticationFilter that is capable of automatically performing an authentication attempt * based on the incoming request.*/public abstract class AuthenticatingFilter extends AuthenticationFilter {

8> FormAuthenticationFilter:shiro提供的用于实现用户登录功能,如果我们打算自己实现登录,那么我们应用 PassThruAuthenticationFilter 来替代

/** * Requires the requesting user to be authenticated for the request to continue, and if they are not, forces the user * to login via by redirecting them to the setLoginUrl(String) you configure. * If you would prefer to handle the authentication validation and login in your own code, consider using the * PassThruAuthenticationFilter instead, which allows requests to the loginUrl to pass through to your application's code directly.*/public class FormAuthenticationFilter extends AuthenticatingFilter {

9> PassThruAuthenticationFilter : 用于我们自己在controller中实现登录逻辑时替代FormAuthenticationFilter

/** * An authentication filter that redirects the user to the login page when they are trying to access * a protected resource. However, if the user is trying to access the login page, the filter lets * the request pass through to the application code. * The difference between this filter and the FormAuthenticationFilter is that * on a login submission (by default an HTTP POST to the login URL), the FormAuthenticationFilter filter * attempts to automatically authenticate the user by passing the username and password request parameter values to * Subject.login(AuthenticationToken) directly. * Conversely, this controller always passes all requests to the loginUrl through, both GETs and POSTs.   * This is useful in cases where the developer wants to write their own login behavior, which should include a * call to Subject.login(AuthenticationToken) at some point.  For example, if the developer has their own custom MVC  * login controller or validator, this PassThruAuthenticationFilter may be appropriate.*/public class PassThruAuthenticationFilter extends AuthenticationFilter {
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { if (isLoginRequest(request, response)) { return true; } else { saveRequestAndRedirectToLogin(request, response); return false; } }}

10> Shiro 自带的filter:

Shiro自身提供了很多的默认filter 来供我们使用,主要分为两种:一是 登录认证相关的filter;一是权限访问控制相关的filter;

登录认证相关的filter有:

1)filter名称: anon, 实现类org.apache.shiro.web.filter.authc.AnonymousFilter,主要用于静态资源的访问,表示无需登录就可以访问;

2)filter名称: authc, 实现类org.apache.shiro.web.filter.authc.FormAuthenticationFilter,主要用于表单登录,没有登录则跳转登录url;

3)filter名称: user, 实现类org.apache.shiro.web.filter.authc.UserFilter,主要用于要求用户已经登录或者通过“记住我”功能登录了也行。

4)filter名称: logout, 实现类org.apache.shiro.web.filter.authc.LogoutFilter,主要用于用户登出

5)filter名称: authcBasic,authc的简化形式,略。

权限访问控制相关的filter有:

1)filter名称: roles, 实现类org.apache.shiro.web.filter.authc.RolesAuthorizationFilter,主要用于验证用户必须拥有某角色,才能继续访问;

2)filter名称: perms, 实现类org.apache.shiro.web.filter.authc.PermissionsAuthorizationFilter,主要用于验证用户必须拥有某权限,才能继续访问;

3)filter名称: ssl, 实现类org.apache.shiro.web.filter.authc.SslFilter,主要用于要求访问协议是https才能访问,不然跳转到https的443短裤;

4)filter名称: port rest noSessionCreation,略。

我们上面的shiroFilter的配置中,已经使用过了上面这些自带的filter:

/reg/** = anon    
/login = authc /logout = logout /authenticated = authc /loginController = anon /js/** = anon /css/** = anon /img/** = anon /html/** = anon /font-awesome/** = anon /** = user

我们看到 /reg/** 注册相关的,/js/**静态资源都是使用的 anon匿名过滤器,不要求用户已经登录就可以访问。

/** = user 放在最后是要求除了上面那些 url 之外的访问路径,都需要登录认证过或者通过记住我登录认证过。因为路径比较是从上面开始列出来的先开始比较的,匹配了就走该过滤器,不会继续下面的过滤器了。

4. shiro的权限标签

Shiro提供了相应的权限标签,用来实现根据用户的角色和权限来显示它相应的菜单和按钮。首先需要导入shiro标签库:

<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>

标签库的定义位于:shiro-web.jar 包中的META-INF/shiro.tld文件中:

1.1.2
1.2
Apache Shiro
http://shiro.apache.org/tags
Apache Shiro JSP Tag Library.
hasPermission
org.apache.shiro.web.tags.HasPermissionTag
JSP
Displays body content only if the current Subject (user) 'has' (implies) the specified permission (i.e the user has the specified ability).
name
true
true
lacksPermission
org.apache.shiro.web.tags.LacksPermissionTag
JSP
Displays body content only if the current Subject (user) does NOT have (not imply) the specified permission (i.e. the user lacks the specified ability)
name
true
true
hasRole
org.apache.shiro.web.tags.HasRoleTag
JSP
Displays body content only if the current user has the specified role.
name
true
true
hasAnyRoles
org.apache.shiro.web.tags.HasAnyRolesTag
JSP
Displays body content only if the current user has one of the specified roles from a comma-separated list of role names.
name
true
true
lacksRole
org.apache.shiro.web.tags.LacksRoleTag
JSP
Displays body content only if the current user does NOT have the specified role (i.e. they explicitly lack the specified role)
name
true
true
authenticated
org.apache.shiro.web.tags.AuthenticatedTag
JSP
Displays body content only if the current user has successfully authenticated _during their current session_. It is more restrictive than the 'user' tag. It is logically opposite to the 'notAuthenticated' tag.
notAuthenticated
org.apache.shiro.web.tags.NotAuthenticatedTag
JSP
Displays body content only if the current user has NOT succesfully authenticated _during their current session_. It is logically opposite to the 'authenticated' tag.
user
org.apache.shiro.web.tags.UserTag
JSP
Displays body content only if the current Subject has a known identity, either from a previous login or from 'RememberMe' services. Note that this is semantically different from the 'authenticated' tag, which is more restrictive. It is logically opposite to the 'guest' tag.
guest
org.apache.shiro.web.tags.GuestTag
JSP
Displays body content only if the current Subject IS NOT known to the system, either because they have not logged in or they have no corresponding 'RememberMe' identity. It is logically opposite to the 'user' tag.
principal
org.apache.shiro.web.tags.PrincipalTag
JSP
Displays the user's principal or a property of the user's principal.
type
false
true
property
false
true
defaultValue
false
true
shiro.tld

其中最重要的标签是关于角色和权限的:

<shiro:hasAnyRoles name="student,teacher"></shiro:hasAnyRoles>

<shiro:hasPermission name="user:delete"></shiro:hashPermission>

其它还有关于登录与否的标签:

<shiro:guest></shiro:guest> 未登录可以显示的信息;

<shiro:user></shiro:user> 用户已经登录或者通过记住我登录后显示的信息;

<shiro:authenticated></shiro:authenticated> 必须是实际登录,不是通过记住我登录的

其它标签参考 shiro.tld文件。

shiro标签使用示例:

效果是根据用户拥有的角色,来显示左侧有哪些菜单项。

5. shiro 权限注解的使用

shiro对权限的控制,除了前面给出的在 shiroFilter这个bean中配置的过滤器:

/reg/** = anon
/login = authc /logout = logout /loginController = anon /js/** = anon /css/** = anon /img/** = anon /html/** = anon /font-awesome/** = anon /** = user

之外,最重要的就是使用注解的方式来进行访问控制的实现了。shiro权限注解可以达到方法级别的细腻控制,可以控制具有某些权限或者某些角色的用户才能访问某个方法(某个url)。先要开启shiro权限注解功能,开启方法参见文档:http://shiro.apache.org/spring.html

Here is how to enable these annotations. Just add these two bean definitions to applicationContext.xml:

开启shiro权限注解的方法二

<aop:config /> 表示开启spring注解,而 DefaultAdvisorAutoProxyCreator 表示会自动创建代理。但是二者最好不要同时使用。

AuthorizationAttributeSourceAdvisor 通过其依赖的 securityManager 来获取用户的角色和权限信息,进而可以进行权限判断。

支持的shiro注解有:

@SuppressWarnings({"unchecked"})public class AuthorizationAttributeSourceAdvisor extends StaticMethodMatcherPointcutAdvisor {    private static final Logger log = LoggerFactory.getLogger(AuthorizationAttributeSourceAdvisor.class);    private static final Class
[] AUTHZ_ANNOTATION_CLASSES = new Class[] { RequiresPermissions.class, RequiresRoles.class, RequiresUser.class, RequiresGuest.class, RequiresAuthentication.class }; protected SecurityManager securityManager = null;     public AuthorizationAttributeSourceAdvisor() {
        setAdvice(new AopAllianceAnnotationsAuthorizingMethodInterceptor());     }

RequiresPermissions, RequiresRolesRequiresUser, RequiresGuest, RequiresAuthentication

主要是通过: AopAllianceAnnotationsAuthorizingMethodInterceptor 类来实现的:

public class AopAllianceAnnotationsAuthorizingMethodInterceptor extends AnnotationsAuthorizingMethodInterceptor implements MethodInterceptor{    public AopAllianceAnnotationsAuthorizingMethodInterceptor() {        List
interceptors = new ArrayList
(5); //use a Spring-specific Annotation resolver - Spring's AnnotationUtils is nicer than the //raw JDK resolution process. AnnotationResolver resolver = new SpringAnnotationResolver(); //we can re-use the same resolver instance - it does not retain state: interceptors.add(new RoleAnnotationMethodInterceptor(resolver)); interceptors.add(new PermissionAnnotationMethodInterceptor(resolver)); interceptors.add(new AuthenticatedAnnotationMethodInterceptor(resolver)); interceptors.add(new UserAnnotationMethodInterceptor(resolver)); interceptors.add(new GuestAnnotationMethodInterceptor(resolver)); setMethodInterceptors(interceptors); }

上面注入了注解的拦截器实现。具体的拦截判断权限过程实现如下:

public class RoleAnnotationHandler extends AuthorizingAnnotationHandler {
public RoleAnnotationHandler() { super(RequiresRoles.class); } public void assertAuthorized(Annotation a) throws AuthorizationException { if (!(a instanceof RequiresRoles)) return; RequiresRoles rrAnnotation = (RequiresRoles) a; String[] roles = rrAnnotation.value(); if (roles.length == 1) { getSubject().checkRole(roles[0]); return; } if (Logical.AND.equals(rrAnnotation.logical())) { getSubject().checkRoles(Arrays.asList(roles)); return; } if (Logical.OR.equals(rrAnnotation.logical())) { // Avoid processing exceptions unnecessarily - "delay" throwing the exception by calling hasRole first boolean hasAtLeastOneRole = false; for (String role : roles) if (getSubject().hasRole(role)) hasAtLeastOneRole = true; // Cause the exception if none of the role match, note that the exception message will be a bit misleading if (!hasAtLeastOneRole) getSubject().checkRole(roles[0]); } }}

主要是上面的方法 assertAuthorized(Annotation a) 中来实现对用户是否拥有某些角色进行判断的。其实还是很简单的。

shiro权限注解使用方法如下所示:

@RequiresPermissions(value={"user:update", "user:select"}, logical= Logical.AND)    @RequestMapping(value="/modifyPassword", method=RequestMethod.POST)    @ResponseBody    public Map
modifyPassword(String oldPassword, String newPassword, HttpSession session) {
Map
map = new HashMap<>(); if(oldPassword == null || newPassword == null || newPassword.length() < 8 || newPassword.length() > 32){ map.put("result", "error"); map.put("msg", "密码必须在8到20位之间"); return map; } User user = (User)session.getAttribute(ConstantConfig.LONGIN_USER); if(user != null){ PasswordHelper ph = new PasswordHelper(); if(!ph.checkPasswordAndEncryptPassword(oldPassword, user)){ // 判断输入的 oldPassword是否正确 map.put("result", "error"); map.put("msg", "密码错误"); return map; }else{ user.setPassword(newPassword); ph.encryptPassword(user); int result = this.userService.updateUserById(user); if(result > 0){ map.put("result", "ok"); map.put("msg", "密码修改成功,请重新登录"); }else{ map.put("result", "error"); map.put("msg", "密码修改失败"); } return map; } } return map; }

@RequiresPermissions(value={"user:update", "user:select"}, logical= Logical.AND)

表示必须有 对 user 表的同时拥有 查询和更新权限,才能修改密码。

转载地址:http://mddix.baihongyu.com/

你可能感兴趣的文章
[算法]基于分区最近点算法的二维平面
查看>>
webpack多页应用架构系列(七):开发环境、生产环境傻傻分不清楚?
查看>>
笨办法学C 练习1:启用编译器
查看>>
树的总结--树的性质(树的深度) leetcode
查看>>
【Android游戏开发之六】在SurfaceView中添加组件!!!!并且相互交互数据!!!!...
查看>>
linux 将大文件分成小文件
查看>>
CCNA- 距离矢量路由协议学习
查看>>
企业实践用户邮箱导入/导出(第2部分)
查看>>
如何学习Linux命令-初级篇
查看>>
从Oracle Public Yum为Oracle Linux建立本地的Yum源
查看>>
静态路由和默认路由
查看>>
关于阿里开发者招聘节 |这5道笔试真题 你会吗!???
查看>>
C#的异常处理机制
查看>>
vsftp:500 OOPS: could not bind listening IPv4 sock
查看>>
Linux安装BTCPayServer并设置比特币BTC和Lightning支付网关
查看>>
mysql安装,远程连接,以及修改密码
查看>>
Mybatis查询返回Map类型数据
查看>>
java的深拷贝与浅拷贝
查看>>
程序员如何提高工作效率
查看>>
promise
查看>>